In React
, it's really common in front-end
development to pass a ref
to a child component, either through ref forwarding
or by creating a ref
with something like const ref = useRef()
, then passing it down. This allows the parent component to directly access the child’s methods and properties.
In Ember
, however, things work a bit differently. Instead of using refs, Ember provides a did-insert
modifier that gives you access to the inserted HTML element. But it only works with native HTML elements. Here’s an example of how it looks:
<div {{did-insert this.handleInsert}}>
<!-- some HTML content -->
</div>
import Component from '@glimmer/component';
import { action } from '@ember/object';
interface Args {
// list of arguments
}
export default class ParentComponent extends Component<Args> {
@action
handleInsert(element: HTMLDivElement) {
// do something with 'element'
}
}
Now, let’s say we want to render another Ember component inside ParentComponent
instead of a div
, like this:
<!--
this.handleInsert won’t ever be called because
did-insert only works with native HTML elements.
-->
<ChildComponent {{did-insert this.handleInsert}} />
The solution is to pass a ref
argument to the child component and include a reference to the child component in its constructor.
import Component from '@glimmer/component';
interface Args {
ref?: (component: ChildComponent)
// list of other arguments
}
export default class ChildComponent extends Component<Args> {
constructor(owner: unknown, args: Args) {
super(owner, args);
// Call 'ref' if it is provided
this.args.ref?.(this);
}
public doSomething() {}
}
Next, update the parent component accordingly:
<!--
'this.handleInsert' won't be ever called,
because 'did-insert' only works for native HTML elements
-->
<ChildComponent @ref={{this.handleChildRef}} />
import Component from '@glimmer/component';
import { action} from '@ember/object';
import { tracked } from '@glimmer/tracking';
interface Args {
// list of arguments
}
export default class ParentComponent extends Component<Args> {
@tracked childRef?: ChildComponent;
@action
handleChildRef(component: ChildComponent) {
this.childRef = component;
}
/**
* You can call methods of the child component from anywhere in parent component.
*/
private someMethod() {
this.childRef?.doSomething();
}
}
By doing this, you expose all the public
methods and properties of the child component to the parent. Since there may be some you don’t want to expose, it’s a good practice to always use the public
, protected
, and private
keywords to ensure only what needs to be accessible is exposed.
If you want more control and prefer to pass a custom set of methods and properties to the parent, you can modify the code as shown below. This is the Ember equivalent of React
’s useImperativeHandle
hook.
import Component from '@glimmer/component';
interface Args {
ref?: (component: ChildComponent)
// list of other arguments
}
export type ChildComponentRef = {
doSomething: () => void;
anotherMethod: () => void;
}
export default class ChildComponent extends Component<Args> {
constructor(owner: unknown, args: Args) {
super(owner, args);
// Call 'ref' if it is provided
this.args.ref?.({
doSomething: this.doSomething.bind(this),
anotherMethod: () => {
// do some stuff
}
});
}
public doSomething() {}
}
And the parent component:
import Component from '@glimmer/component';
import { action} from '@ember/object';
import { tracked } from '@glimmer/tracking';
interface Args {
// list of arguments
}
export default class ParentComponent extends Component<Args> {
@tracked childRef?: ChildComponentRef;
@action
handleChildRef(component: ChildComponentRef) {
this.childRef = component;
}
/**
* You can call methods of the child component from anywhere in parent component.
*/
private someMethod() {
this.childRef?.doSomething();
thid.childRef?.anotherMethod();
}
}
Job done! Thanks for reading.